Инструмент для просмотра машинного кода .net и исполняемых файлов
https://github.com/dnSpyEx/dnSpy/releases/tag/v6.5.1
Очень удобно
Каждая секция содержит в начале поля:
Общая структура файла такая:
#~ или #- |
Таблицы метаданных (compressed / uncompressed формат) |
---|---|
#Strings |
Таблица строк (имена классов, методов и пр.) |
#US |
User Strings (строковые литералы из IL) |
#Blob |
Сигнатуры методов, массивы, параметры |
#GUID |
GUID'ы сборки |
Отдельно про поток #~
в нем хранятся таблицы метаданных
Каждая таблица — это структурированный набор записей, похожий на строки базы данных. Каждая строка описывает что-то: тип, метод, атрибут и т.д.
Таблица | Назначение |
---|---|
00 Module | Информация о текущем модуле (сборке .dll/.exe). Одна строка. |
01 TypeRef | Ссылки на типы, определённые в других сборках. |
02 TypeDef | Определения всех типов в этой сборке. |
06 MethodDef | Все методы, определённые в сборке. |
08 Param | Описания параметров методов. |
0A MemberRef | Ссылки на поля/методы, определённые вне текущей сборки. |
0C CustomAttribute | Все кастомные атрибуты (например, [Serializable] ). |
11 StandAloneSig | Сигнатуры (например, для лямбд или delegate). |
1B TypeSpec | Специализации generic-типов (например, List<int> ). |
20 Assembly | Информация о сборке (имя, версия и пр.). |
23 AssemblyRef | Ссылки на внешние сборки. |
2B MethodSpec | Generic-специализации методов (Do<T>() ). |
Для обращения к метаданных из IL кода применяются токены
формат токена
Токен = [таблица или поток (8 бит) ] + [индекс для таблиц/смещение для потока со строками(24 бита)]
Таблицы представлены буквально как таблицы в постгре, то есть идут записи подряд
структура каждой таблицы метаданных в потоке #~
жёстко задана стандартом ECMA-335 — то есть она не описывается в самом PE-файле
Пример структуры таблицы:
TypeDef Row Layout (обычно):
В .Net домены приложений, AppDomains - это изначально механизм изоляции выполнения программ внутри одного процесса. Данный механизм реализовывался на уровне CLR.
AppDomain - это логически изолированная среда. Она:
Осталось:
Что удалено:
Применяются отдельные процессы и AssemblyLoadContext()
Теперь таким образом можно создавать свои контексты сборки и загружать туда длл в виде файла.
пример кода с загрузкой длл
var context = new AssemblyLoadContext("PluginContext", isCollectible: true);
var assembly = context.LoadFromAssemblyPath("/plugins/MyPlugin.dll");
var type = assembly.GetType("MyPluginNamespace.MyPluginClass");
var instance = Activator.CreateInstance(type);
Процесс выгрузки сборки, сработает только если isCollectible: true
var context = new AssemblyLoadContext("UnloadableContext", isCollectible: true);
Assembly assembly = context.LoadFromAssemblyPath("/path/to.dll");
// Работа с типами...
context.Unload(); // Помечает на выгрузку
GC.Collect(); GC.WaitForPendingFinalizers(); // Чтобы реально выгрузилось
⚠️ Важно: выгрузка работает только при отсутствии живых ссылок на типы из загруженной сборки.
По умолчанию .NET использует AssemblyLoadContext.Default
.
Когда вы создаёте новый AssemblyLoadContext
, CLR создает изолированное пространство загрузки, где типы и сборки не пересекаются с другими контекстами.
Может быть сборкой-разгружаемым (isCollectible: true
) или обычным (в памяти навсегда).
Характеристика | AppDomain (.NET Framework) | AssemblyLoadContext (.NET Core / .NET 5+) |
---|---|---|
Изоляция памяти | Частичная — данные и стеки изолированы | Только изоляция сборок (DLL), память общая |
Изоляция объектов | Полная: нельзя передать объекты напрямую | Нет: объекты общие, если типы совпадают |
Механизм безопасности (CAS херня для безопасности кода из винды) | Поддерживает CAS (ограничения прав) | Не поддерживается |
Граница сериализации | Да (можно передавать только сериализуемые) | Нет: объект = объект |
Сборка мусора | Уничтожается вся область домена | Выгружается только если isCollectible = true |
Тип определяется не только по имени, но и по контексту загрузки сборки.
Домен это мега изоляция почти как процессы, но на уровне CLR, но все это обрезали в новых версиях
В рамках одного процесса может быть несколько контекстов, в рамках одного контекста свои переменные и типы. Типы и переменные из двух разных контекстов взаимодействовать друг с другом не могут.
Но! В рамках одного контекста может быть несколько сборок (То есть DLL), и переменные между ними могут взаимодействовать, но нужно учитывать что описание типа - не только название но и сборка. Поэтому, например для каста между разными сборками, можно сделать сборку contracts.dll где описать общие для этих двоих интерфейсы и касты между ними.
Пример кастов между сборками в рамках одного контекста:
var context = new AssemblyLoadContext("MyContext");
var contracts = context.LoadFromAssemblyPath("Contracts.dll");
var pluginA = context.LoadFromAssemblyPath("PluginA.dll");
var pluginB = context.LoadFromAssemblyPath("PluginB.dll");
var typeA = pluginA.GetType("PluginA.MyPlugin");
var typeB = pluginB.GetType("PluginB.MyPlugin");
var objA = Activator.CreateInstance(typeA);
var objB = Activator.CreateInstance(typeB);
// Получаем Type интерфейса из загруженной Contracts.dll
var interfaceType = contracts.GetType("Contracts.IPlugin");
// ✅ Кастинг сработает, потому что это один и тот же Type
bool isAPlugin = interfaceType.IsInstanceOfType(objA); // true
bool isBPlugin = interfaceType.IsInstanceOfType(objB); // true
// Можно привести:
var plugin = (dynamic)objA;
plugin.Run(); // если метод Run есть
Существование контекстов обусловлено системами, которые могут одновременно использовать библиотеки разных версий, и чтобы изолировать типы этих библиотек, их можно подключать в разных контекстах.
Commited память включает в себя ту память, которую ОС выложила на реальную, но возможно на swap возможно в рам
Private WS это память которая выложена в именно в РАМ и которая не общая
Shareable (около 2 ГиБ) – разделяемая память, которая нас не особенно интересует; Эти области служат для целей системного управления, вообще не имеющих отношения к .NET;
Mapped File (около 4 МиБ) – как отмечалось в главе 2, эти области содержат проецируемые файлы, в частности шрифты и файлы локализации. Хотя они и читаются средой выполнения .NET с применением различных API локализации, никаких проблем нашему приложению они создавать не должны;
Image (около 37 МиБ) – двоичные образы, содержащие различные исполняемые файлы .NET, включая саму среду выполнения и нашу сборку. Отметим, что большая часть этой области разделяемая, и лишь 772 КиБ входят в частный рабочий набор. Это файлы, которые читаются с диска на этапе запуска приложения;
Stack (около 4,5 МиБ) – в нашем приложении Hello World три потока, поэтому для них отведено три области под стеки;
Heap и Private Data (около 9 МиБ) – это различные области памяти, которые среда выполнения .NET использует для собственных целей. Среди них есть фундаментальные структуры данных например:
Если по какой-то причине в нашем приложении часто производится JIT-компиляция, то мы будем наблюдать постоянный рост таких частных областей с флагами Выполнение/Чтение/ Запись;
Page Table (небольшая область размером 36 КиБ) – таблица страниц
Существует стек вызова, это наш привычный стек, он делится на фреймы. Фрейм под каждый метод. Каждый фрейм состоит из 4 логических участков памяти:
Существует мнение, что тип значений хранится на стеке, а типы ссылочные на куче. Это бред.
Разница в том, что тип значений содержит сразу всю необходимую информацию в своем описании. Например инт содержит все что нужно сразу в себе.
А ссылочные типы содержат только ссылку.
А то, где хранится тип, зависит от случая
Тип значений может хранится как на стеке так и на куче. Это зависит от контекста, пример:
По причине того, что структуры могут попасть в регистры, что они могут хранится на стеке, что они не наследуются, а еще что при хранении у них нет доп данных, только поля, они могут очень сильно оптимизировать код (Например пользуясь предсказуемостью структур, JIT может их сам оптимизировать как хочет)
(Важно, что структуры не хранят в памяти рядом с собой никаких доп информаций, даже ссылки на описание класса, только поля. Для вызова метода напрямую пишется команда на вызов метода из таблицы методов)
В отличие от структуры, экземпляр класса содержит много дополнительной информации, а именно
заголовок объекта – место для «любой дополнительной информации. Часто заголовок просто содержит нули, но обычно он используется для хранения информации о блокировке, поставленной на объект, или для кеширования значения, вычисленного методом GetHashCode. Заголовок используется по принципу «кто первый встал, того и тапки». Если среде выполнения он понадобился для хранения блокировки, то хеш-код в нем уже не хранится.
ссылка на таблицу методов – как уже было сказано, «тип объекта явно хранится в его представлении», и, с точки зрения реализации, это и есть таблица методов (MethodTable). Именно сюда указывают все внешние ссылки на объект, т. е. если на объект имеются какие-то ссылки, то все они содержат адрес хранящейся в нем ссылки на таблицу методов. Поэтому говорят, что заголовок объекта имеет «отрицательный индекс». Ссылка на таблицу методов сама является указателем на часть структуры, содержащей описание типа (она находится в высокочастотной куче домена, содержащего этот тип);
факультативный заместитель данных, если в типе нет ни одного поля. Сборщик мусора настаивает на том, чтобы в каждом объекте было место хотя бы для одного поля такой длины, как указатель. Это поле используется для разных целей, но каких - сложно.
Таким образом, в 64 разрядной системе, минимальный размер экземпляра объекта равен 24 байта
8 байт для заголовка объекта – из которых на самом деле используются только 4, а остальные 4 заполнены нулями (т. к. в 64-разрядной архитектуре область памяти должна быть выровнена на 8-байтовую границу);
8 байт (размер указателя) для ссылки на таблицу методов;
8 байт (размер указателя) для внутреннего заместителя данных.
Чуть глубже про интернирование, в CLR в неуправляемой куче лежит структура, словарь строк с адресом внутри другой структуры. Другая структура это тоже словарь, но уже в куче больших объектов, и уже в ней лежат ссылки на строки как на обычные объекты.
В случае если параметр метода принимает интерфейс, то при передаче структуры реализующей этот интерфейс, происходит упаковка, что плохо. Для избегания этого, можно применить финт:
void Method<T>(T parametr) where T : ISomeInterface
В таком случае, во время исполнения, для передаваемой структуры будет сгенерирована реализация как для структуры и упаковки не будет.
==ВАЖНО! Для List при вызове GetEnumerator() сам инумератор является структурой, а значит происходит упаковка в foreach, за этим надо следить, и если много форычей, то делать финг выше ==